<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Auth
 * @subpackage Adapter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 * @version $Id: DbTable.php 24593 2012-01-05 20:35:02Z matthew $
 */
/**
 *
 * @see Zend_Auth_Adapter_Interface
 */
require_once 'Zend/Auth/Adapter/Interface.php';
/**
 *
 * @see Zend_Db_Adapter_Abstract
 */
require_once 'Zend/Db/Adapter/Abstract.php';
/**
 *
 * @see Zend_Auth_Result
 */
require_once 'Zend/Auth/Result.php';

/**
 *
 * @category Zend
 * @package Zend_Auth
 * @subpackage Adapter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Auth_Adapter_DbTable implements Zend_Auth_Adapter_Interface {

    /**
     * Database Connection
     * @var Zend_Db_Adapter_Abstract
     */
    protected $_zendDb = null;

    /**
     *
     * @var Zend_Db_Select
     */
    protected $_dbSelect = null;

    /**
     * $_tableName - the table name to check
     * @var string
     */
    protected $_tableName = null;

    /**
     * $_identityColumn - the column to use as the identity
     * @var string
     */
    protected $_identityColumn = null;

    /**
     * $_credentialColumns - columns to be used as the credentials
     * @var string
     */
    protected $_credentialColumn = null;

    /**
     * $_identity - Identity value
     * @var string
     */
    protected $_identity = null;

    /**
     * $_credential - Credential values
     * @var string
     */
    protected $_credential = null;

    /**
     * $_credentialTreatment - Treatment applied to the credential, such as MD5() or PASSWORD()
     * @var string
     */
    protected $_credentialTreatment = null;

    /**
     * $_authenticateResultInfo
     * @var array
     */
    protected $_authenticateResultInfo = null;

    /**
     * $_resultRow - Results of database authentication query
     * @var array
     */
    protected $_resultRow = null;

    /**
     * $_ambiguityIdentity - Flag to indicate same Identity can be used with
     * different credentials.
     * Default is FALSE and need to be set to true to
     * allow ambiguity usage.
     * @var boolean
     */
    protected $_ambiguityIdentity = false;

    /**
     * __construct() - Sets configuration options
     * @param Zend_Db_Adapter_Abstract $zendDb If null, default database adapter assumed
     * @param string $tableName
     * @param string $identityColumn
     * @param string $credentialColumn
     * @param string $credentialTreatment
     * @return void
     */
    public function __construct (Zend_Db_Adapter_Abstract $zendDb = null, $tableName = null, $identityColumn = null, $credentialColumn = null, $credentialTreatment = null) {
        $this -> _setDbAdapter ($zendDb);
        if (null !== $tableName) {
            $this -> setTableName ($tableName);
        }
        if (null !== $identityColumn) {
            $this -> setIdentityColumn ($identityColumn);
        }
        if (null !== $credentialColumn) {
            $this -> setCredentialColumn ($credentialColumn);
        }
        if (null !== $credentialTreatment) {
            $this -> setCredentialTreatment ($credentialTreatment);
        }
    }

    /**
     * _setDbAdapter() - set the database adapter to be used for quering
     * @param Zend_Db_Adapter_Abstract
     * @throws Zend_Auth_Adapter_Exception
     * @return Zend_Auth_Adapter_DbTable
     */
    protected function _setDbAdapter (Zend_Db_Adapter_Abstract $zendDb = null) {
        $this -> _zendDb = $zendDb;
        /**
         * If no adapter is specified, fetch default database adapter.
         */
        if (null === $this -> _zendDb) {
            require_once 'Zend/Db/Table/Abstract.php';
            $this -> _zendDb = Zend_Db_Table_Abstract::getDefaultAdapter ();
            if (null === $this -> _zendDb) {
                require_once 'Zend/Auth/Adapter/Exception.php';
                throw new Zend_Auth_Adapter_Exception ('No database adapter present');
            }
        }
        return $this;
    }

    /**
     * setTableName() - set the table name to be used in the select query
     * @param string $tableName
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setTableName ($tableName) {
        $this -> _tableName = $tableName;
        return $this;
    }

    /**
     * setIdentityColumn() - set the column name to be used as the identity column
     * @param string $identityColumn
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setIdentityColumn ($identityColumn) {
        $this -> _identityColumn = $identityColumn;
        return $this;
    }

    /**
     * setCredentialColumn() - set the column name to be used as the credential column
     * @param string $credentialColumn
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setCredentialColumn ($credentialColumn) {
        $this -> _credentialColumn = $credentialColumn;
        return $this;
    }

    /**
     * setCredentialTreatment() - allows the developer to pass a parameterized string that is
     * used to transform or treat the input credential data.
     * In many cases, passwords and other sensitive data are encrypted, hashed, encoded,
     * obscured, or otherwise treated through some function or algorithm. By specifying a
     * parameterized treatment string with this method, a developer may apply arbitrary SQL
     * upon input credential data.
     * Examples:
     * 'PASSWORD(?)'
     * 'MD5(?)'
     * @param string $treatment
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setCredentialTreatment ($treatment) {
        $this -> _credentialTreatment = $treatment;
        return $this;
    }

    /**
     * setIdentity() - set the value to be used as the identity
     * @param string $value
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setIdentity ($value) {
        $this -> _identity = $value;
        return $this;
    }

    /**
     * setCredential() - set the credential value to be used, optionally can specify a treatment
     * to be used, should be supplied in parameterized form, such as 'MD5(?)' or 'PASSWORD(?)'
     * @param string $credential
     * @return Zend_Auth_Adapter_DbTable Provides a fluent interface
     */
    public function setCredential ($credential) {
        $this -> _credential = $credential;
        return $this;
    }

    /**
     * setAmbiguityIdentity() - sets a flag for usage of identical identities
     * with unique credentials.
     * It accepts integers (0, 1) or boolean (true,
     * false) parameters. Default is false.
     * @param int|bool $flag
     * @return Zend_Auth_Adapter_DbTable
     */
    public function setAmbiguityIdentity ($flag) {
        if (is_integer ($flag)) {
            $this -> _ambiguityIdentity = (1 === $flag ? true : false);
        } elseif (is_bool ($flag)) {
            $this -> _ambiguityIdentity = $flag;
        }
        return $this;
    }

    /**
     * getAmbiguityIdentity() - returns TRUE for usage of multiple identical
     * identies with different credentials, FALSE if not used.
     * @return bool
     */
    public function getAmbiguityIdentity () {
        return $this -> _ambiguityIdentity;
    }

    /**
     * getDbSelect() - Return the preauthentication Db Select object for userland select query modification
     * @return Zend_Db_Select
     */
    public function getDbSelect () {
        if ($this -> _dbSelect == null) {
            $this -> _dbSelect = $this -> _zendDb -> select ();
        }
        return $this -> _dbSelect;
    }

    /**
     * getResultRowObject() - Returns the result row as a stdClass object
     * @param string|array $returnColumns
     * @param string|array $omitColumns
     * @return stdClass boolean
     */
    public function getResultRowObject ($returnColumns = null, $omitColumns = null) {
        if ( ! $this -> _resultRow) {
            return false;
        }
        $returnObject = new stdClass ();
        if (null !== $returnColumns) {
            $availableColumns = array_keys ($this -> _resultRow);
            foreach ((array) $returnColumns as $returnColumn) {
                if (in_array ($returnColumn, $availableColumns)) {
                    $returnObject -> {$returnColumn} = $this -> _resultRow[$returnColumn];
                }
            }
            return $returnObject;
        } elseif (null !== $omitColumns) {
            $omitColumns = (array) $omitColumns;
            foreach ($this -> _resultRow as $resultColumn => $resultValue) {
                if ( ! in_array ($resultColumn, $omitColumns)) {
                    $returnObject -> {$resultColumn} = $resultValue;
                }
            }
            return $returnObject;
        } else {
            foreach ($this -> _resultRow as $resultColumn => $resultValue) {
                $returnObject -> {$resultColumn} = $resultValue;
            }
            return $returnObject;
        }
    }

    /**
     * authenticate() - defined by Zend_Auth_Adapter_Interface.
     * This method is called to
     * attempt an authentication. Previous to this call, this adapter would have already
     * been configured with all necessary information to successfully connect to a database
     * table and attempt to find a record matching the provided identity.
     * @throws Zend_Auth_Adapter_Exception if answering the authentication query is impossible
     * @return Zend_Auth_Result
     */
    public function authenticate () {
        $this -> _authenticateSetup ();
        $dbSelect = $this -> _authenticateCreateSelect ();
        $resultIdentities = $this -> _authenticateQuerySelect ($dbSelect);
        if (($authResult = $this -> _authenticateValidateResultSet ($resultIdentities)) instanceof Zend_Auth_Result) {
            return $authResult;
        }
        if (true === $this -> getAmbiguityIdentity ()) {
            $validIdentities = array();
            $zendAuthCredentialMatchColumn = $this -> _zendDb -> foldCase ('zend_auth_credential_match');
            foreach ($resultIdentities as $identity) {
                if (1 === (int) $identity[$zendAuthCredentialMatchColumn]) {
                    $validIdentities[] = $identity;
                }
            }
            $resultIdentities = $validIdentities;
        }
        $authResult = $this -> _authenticateValidateResult (array_shift ($resultIdentities));
        return $authResult;
    }

    /**
     * _authenticateSetup() - This method abstracts the steps involved with
     * making sure that this adapter was indeed setup properly with all
     * required pieces of information.
     * @throws Zend_Auth_Adapter_Exception - in the event that setup was not done properly
     * @return true
     */
    protected function _authenticateSetup () {
        $exception = null;
        if ($this -> _tableName == '') {
            $exception = 'A table must be supplied for the Zend_Auth_Adapter_DbTable authentication adapter.';
        } elseif ($this -> _identityColumn == '') {
            $exception = 'An identity column must be supplied for the Zend_Auth_Adapter_DbTable authentication adapter.';
        } elseif ($this -> _credentialColumn == '') {
            $exception = 'A credential column must be supplied for the Zend_Auth_Adapter_DbTable authentication adapter.';
        } elseif ($this -> _identity == '') {
            $exception = 'A value for the identity was not provided prior to authentication with Zend_Auth_Adapter_DbTable.';
        } elseif ($this -> _credential === null) {
            $exception = 'A credential value was not provided prior to authentication with Zend_Auth_Adapter_DbTable.';
        }
        if (null !== $exception) {
            /**
             *
             * @see Zend_Auth_Adapter_Exception
             */
            require_once 'Zend/Auth/Adapter/Exception.php';
            throw new Zend_Auth_Adapter_Exception ($exception);
        }
        $this -> _authenticateResultInfo = array('code' => Zend_Auth_Result::FAILURE, 'identity' => $this -> _identity, 'messages' => array());
        return true;
    }

    /**
     * _authenticateCreateSelect() - This method creates a Zend_Db_Select object that
     * is completely configured to be queried against the database.
     * @return Zend_Db_Select
     */
    protected function _authenticateCreateSelect () {
        // build credential expression
        if (empty ($this -> _credentialTreatment) || (strpos ($this -> _credentialTreatment, '?') === false)) {
            $this -> _credentialTreatment = '?';
        }
        $credentialExpression = new Zend_Db_Expr ('(CASE WHEN ' . $this -> _zendDb -> quoteInto ($this -> _zendDb -> quoteIdentifier ($this -> _credentialColumn, true) . ' = ' . $this -> _credentialTreatment, $this -> _credential) . ' THEN 1 ELSE 0 END) AS ' . $this -> _zendDb -> quoteIdentifier ($this -> _zendDb -> foldCase ('zend_auth_credential_match')));
        // get select
        $dbSelect = clone $this -> getDbSelect ();
        $dbSelect -> from ($this -> _tableName, array('*', $credentialExpression)) -> where ($this -> _zendDb -> quoteIdentifier ($this -> _identityColumn, true) . ' = ?', $this -> _identity);
        return $dbSelect;
    }

    /**
     * _authenticateQuerySelect() - This method accepts a Zend_Db_Select object and
     * performs a query against the database with that object.
     * @param Zend_Db_Select $dbSelect
     * @throws Zend_Auth_Adapter_Exception - when an invalid select
     *         object is encountered
     * @return array
     */
    protected function _authenticateQuerySelect (Zend_Db_Select $dbSelect) {
        try {
            if ($this -> _zendDb -> getFetchMode () != Zend_DB::FETCH_ASSOC) {
                $origDbFetchMode = $this -> _zendDb -> getFetchMode ();
                $this -> _zendDb -> setFetchMode (Zend_DB::FETCH_ASSOC);
            }
            $resultIdentities = $this -> _zendDb -> fetchAll ($dbSelect);
            if (isset ($origDbFetchMode)) {
                $this -> _zendDb -> setFetchMode ($origDbFetchMode);
                unset ($origDbFetchMode);
            }
        } catch (Exception $e) {
            /**
             *
             * @see Zend_Auth_Adapter_Exception
             */
            require_once 'Zend/Auth/Adapter/Exception.php';
            throw new Zend_Auth_Adapter_Exception ('The supplied parameters to Zend_Auth_Adapter_DbTable failed to ' . 'produce a valid sql statement, please check table and column names ' . 'for validity.', 0, $e);
        }
        return $resultIdentities;
    }

    /**
     * _authenticateValidateResultSet() - This method attempts to make
     * certain that only one record was returned in the resultset
     * @param array $resultIdentities
     * @return true Zend_Auth_Result
     */
    protected function _authenticateValidateResultSet (array $resultIdentities) {
        if (count ($resultIdentities) < 1) {
            $this -> _authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
            $this -> _authenticateResultInfo['messages'][] = 'A record with the supplied identity could not be found.';
            return $this -> _authenticateCreateAuthResult ();
        } elseif (count ($resultIdentities) > 1 && false === $this -> getAmbiguityIdentity ()) {
            $this -> _authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS;
            $this -> _authenticateResultInfo['messages'][] = 'More than one record matches the supplied identity.';
            return $this -> _authenticateCreateAuthResult ();
        }
        return true;
    }

    /**
     * _authenticateValidateResult() - This method attempts to validate that
     * the record in the resultset is indeed a record that matched the
     * identity provided to this adapter.
     * @param array $resultIdentity
     * @return Zend_Auth_Result
     */
    protected function _authenticateValidateResult ($resultIdentity) {
        $zendAuthCredentialMatchColumn = $this -> _zendDb -> foldCase ('zend_auth_credential_match');
        if ($resultIdentity[$zendAuthCredentialMatchColumn] != '1') {
            $this -> _authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
            $this -> _authenticateResultInfo['messages'][] = 'Supplied credential is invalid.';
            return $this -> _authenticateCreateAuthResult ();
        }
        unset ($resultIdentity[$zendAuthCredentialMatchColumn]);
        $this -> _resultRow = $resultIdentity;
        $this -> _authenticateResultInfo['code'] = Zend_Auth_Result::SUCCESS;
        $this -> _authenticateResultInfo['messages'][] = 'Authentication successful.';
        return $this -> _authenticateCreateAuthResult ();
    }

    /**
     * _authenticateCreateAuthResult() - Creates a Zend_Auth_Result object from
     * the information that has been collected during the authenticate() attempt.
     * @return Zend_Auth_Result
     */
    protected function _authenticateCreateAuthResult () {
        return new Zend_Auth_Result ($this -> _authenticateResultInfo['code'], $this -> _authenticateResultInfo['identity'], $this -> _authenticateResultInfo['messages']);
    }

}
